这些模式中的许多都可以直接使用DPC++的内置函数，或用DPC++使用供应商提供的库来编写。利用这些功能和库是在真正的大型软件工程项目中，是平衡性能、可移植性和生产力的最佳方法。\par

\hspace*{\fill} \par %插入空行
\textbf{DPC++中的归约库}

DPC++不要求每个人都维护自己的归约内核库，而是提供了一个抽象来用归约语义描述变量。这种抽象简化了内核的表达式，并使显式执行成为可能，允许实现为不同的设备、数据类型和归约操作组合选择不同的归约算法。\par

\hspace*{\fill} \par %插入空行
图14-8 reduce表示为使用归约库的ND-Range数据并行内核
\begin{lstlisting}[caption={}]
h.parallel_for(
	nd_range<1>{N, B},
	reduction(sum, plus<>()),
	[=](nd_item<1> it, auto& sum) {
		int i = it.get_global_id(0);
		sum += data[i];
});
\end{lstlisting}

图14-8中的内核展示了使用归约库的示例。注意，内核体不包含任何对归约的引用——必须指定包含归约的内核，使用加法算子组合了sum变量的实例。这为实现自动生成优化的归约序列提供了足够的信息。\par

编写本文时，归约库只支持具有单个归约变量的内核。未来版本的DPC++预计将支持同时执行多个归约的内核，方法是在传递给parallel\_for的nd\_range和函数参数之间指定多个归约，并将多个归约作为内核函数的参数。\par

归约的结果在内核完成之前不能保证会写回到原来的变量。除了这个限制，访问归约的结果与访问SYCL中的任何其他变量的行为相同:访问存储在缓冲区中的归约结果需要创建适当的设备或主机访问器，访问存储在USM分配中的归约结果可能需要显式的同步和/或内存移动。\par

DPC++的归约库与其他语言中的归约抽象的区别是，限制了在内核执行期间对归约变量的访问——不能检查归约变量的中间值，禁止使用除了指定的组合函数以外的任何东西来更新归约变量。这些限制可以避免一些难以调试的错误(例如，在计算最大值的同时添加一个归约变量)，并确保归约可以在各种不同的设备上高效地实现。\par

\hspace*{\fill} \par %插入空行
\textbf{归约类}

归约类是用来描述内核中归约的接口。构造归约对象的唯一方法是使用图14-9所示的函数。\par

\hspace*{\fill} \par %插入空行
图14-9 归约函数的原型
\begin{lstlisting}[caption={}]
template <typename T, typename BinaryOperation>
unspecified reduction(T* variable, BinaryOperation combiner);

template <typename T, typename BinaryOperation>
unspecified reduction(T* variable, T identity, BinaryOperation combiner);
\end{lstlisting}

该函数的第一个版本允许指定归约变量和用于组合每个工作项的操作符。第二个版本允许提供与归约操作符相关联的可选标识值——这是对用户定义归约的优化。\par

请注意，归约函数的返回类型未指定，归约类本身完全由实现定义。虽然对于C++类来说有点不寻常，但它允许实现使用不同的类(或具有任意数量模板参数的单个类)来表示不同的约简算法。未来版本的DPC++可能会重新考虑这个设计，以便在特定的执行上下文中显式地请求特定的归约算法。\par

\hspace*{\fill} \par %插入空行
\textbf{reducer类}

reducer类的实例封装了一个归约变量，暴露了有限的接口，就可以进行安全的归约变量更新。reducer类的简化定义如图14-10所示。像归约类一样，reducer类是实现定义的——reducer的类型取决于reduce是如何执行的，为了最大化性能，在编译时知道这一点很重要。允许更新归约变量的函数和操作符是定义好的，并且保证DPC++可以支持。\par

\hspace*{\fill} \par %插入空行
图14-10 reducer类的简化定义
\begin{lstlisting}[caption={}]
template <typename T,
		  typename BinaryOperation,
	      /* implementation-defined */>
class reducer {
	// Combine partial result with reducer's value
	void combine(const T& partial);
};

// Other operators are available for standard binary operations
template <typename T>
auto& operator +=(reducer<T,plus::<T>>&, const T&);
\end{lstlisting}

每个reducer都提供一个combine()，它将部分结果(来自单个工作项)与reducer变量的值组合在一起。这个组合函数的行为由实现定义，但不是在编写内核时需要考虑的问题。还需要一个reducer，使其他操作可依赖于reducer变量，例如：+=运算符定义为加归约。这些附加操作符仅作为开发者方便和提高可读性提供，操作符具有与直接调用combine()相同的行为。\par

\hspace*{\fill} \par %插入空行
\textbf{用户定义的归约}

一些常见的归算法(例如，树状归约)不会看到每个工作项直接更新单个共享变量，而是将一些部分结果累加到私有变量中，这些私有变量将在将来的某个时候合并在一起。这样的私有变量引入了一个问题:实现应该如何初始化?从每个工作项初始化到第一个贡献的变量有潜在的性能后果，因为需要额外的逻辑来检测和处理未初始化的变量。将变量初始化为归运算符的标识来避免性能损失，但只有当标识已知时才有可能。\par

DPC++实现只能在还原操作简单的算术类型，且还原操作符是标准函数(例如，plus)时自动确定要使用的正确标识值。对于用户定义的归约(例如，对用户定义的类型进行操作和/或使用用户定义的函数)，可以通过指定标识来提高性能。\par

\hspace*{\fill} \par %插入空行
图14-11 使用用户定义的归约查找具有ND-Range内核的最小值的位置
\begin{lstlisting}[caption={}]
template <typename T, typename I>
struct pair {
	bool operator<(const pair& o) const {
		return val <= o.val || (val == o.val && idx <= o.idx);
	}
	T val;
	I idx;
};

template <typename T, typename I>
using minloc = minimum<pair<T, I>>;

constexpr size_t N = 16;
constexpr size_t L = 4;

queue Q;
float* data = malloc_shared<float>(N, Q);
pair<float, int>* res = malloc_shared<pair<float, int>>(1, Q);
std::generate(data, data + N, std::mt19937{});

pair<float, int> identity = {
	std::numeric_limits<float>::max(), std::numeric_limits<int>::min()
};
*res = identity;

auto red = reduction(res, identity, minloc<float, int>());

Q.submit([&](handler& h) {
	h.parallel_for(nd_range<1>{N, L}, red, [=](nd_item<1> item, auto& res) {
		int i = item.get_global_id(0);
		pair<float, int> partial = {data[i], i};
		res.combine(partial);
	});
}).wait();

std::cout << "minimum value = " << res->val << " at " << res->idx << "\n";
\end{lstlisting}

对用户定义归约的支持，仅限于简单的可复制类型和没有副作用的组合函数，但这足以支持许多实际用例。例如，图14-11中的代码演示了如何使用用户定义的归约来计算向量中的最小元素及其位置。\par

\hspace*{\fill} \par %插入空行
\textbf{oneAPI DPC++库}

C++标准模板库(STL)包含了几种算法，对应于本章讨论的并行模式。STL中的算法通常适用于由迭代器指定的序列。从C++17开始，可以使用执行策略参数，表示算法应该顺序执行还是并行执行。\par

oneAPI DPC++库(oneDPL)利用了这个执行策略参数来提供高效的并行编程方法，这种方法在底层利用了用DPC++编写的内核。如果应用程序可以仅使用STL算法的功能来表达，那么oneDPL就可以在系统中使用加速器，而无需编写任何DPC++内核代码!\par

图14-12中的表格显示了STL中可用的算法如何与本章描述的并行模式以及在适当的情况下与串行算法(在C++17之前可用)相关。关于如何在DPC++应用程序中，使用这些算法的更详细的说明可以在第18章找到。\par

\hspace*{\fill} \par %插入空行
图14-12 将并行模式与C++17算法库相关联
\begin{table}[H]
	\begin{tabular}{|l|l|l|}
		\hline
		\textbf{模式} & \textbf{串行算法} & \textbf{并行算法}                                                                                                      \\ \hline
		Map              & transform                    & transform                                                                                                                           \\ \hline
		Stencil          & transform                    & transform                                                                                                                           \\ \hline
		Reduction        & accumulate                   & \begin{tabular}[c]{@{}l@{}}reduce\\ transform\_reduc\end{tabular}                                                                   \\ \hline
		Scan             & partial\_sum                 & \begin{tabular}[c]{@{}l@{}}inclusive\_scan\\ exclusive\_scan\\ transform\_inclusive\_scan\\ transform\_exclusive\_scan\end{tabular} \\ \hline
		Pack             & N/A                          & copy\_if                                                                                                                            \\ \hline
		Unpack           & N/A                          & N/A                                                                                                                                 \\ \hline
	\end{tabular}
\end{table}

\hspace*{\fill} \par %插入空行
\textbf{组函数}

DPC++设备代码中对并行模式的支持是由单独的组函数库提供的。这些组函数利用特定工作项组(例如，一个工作组或一个子工作组)的并行性，在有限的范围内实现通用的并行算法，并且可以作为构建块来构建更复杂的算法。\par

如oneDPL一样，DPC++中组函数的语法基于C++。每个函数的第一个参数接受一个group或sub\_group对象来代替执行策略，C++算法的任何限制都适用。组功能是由指定组中的所有工作项协作执行的，因此必须以类似于组栅栏的方式对待——组中的所有工作项必须在控制流中遇到相同的算法(即，组中的所有工作项必须以类似的方式执行或不执行到算法调用)，并且所有工作项必须提供相同的参数，以确保操作的一致性。\par

reduce、exclusive\_scan和inclusive\_scan函数仅限于基本数据类型和最常用的归约操作符(例如，plus、minimum和maximum)。这对于许多用例来说已经足够了，但DPC++的未来版本预计将扩展对用户定义类型和操作符的支持。\par

